上一篇我們已經知道,只要有函式就有閉包,而其中一種將閉包運用得淋漓盡致的方法,就是所謂的模組(Modules)。
模組就像一個設計好的工具箱,比方說料理工具箱、雕刻工具箱、水電工具箱,一個模組對應一整套功能,需要某樣工具時,就從對應的工具箱裡拿出來使用。
對外界來說,工具箱的內部是隱藏的,但如果知道打開的方法,也知道該如何操作這些工具,工具箱就能為你打造各種各樣的東西。
如果沒有做到「封閉」這點,讓工具箱內的東西被拿去隨意使用.......你可以想像小孩子拿精密的測量儀亂摔亂打,更糟糕的是,拿金屬起子去戳插座,導致完全無法收拾的後果。
而函數擁有的「閉包(Closure)」,就是在提供取用方法的前提下,避免汙染全域的問題。閉包封裝了函式內部的功能,將方法和變數限制在一個範圍內存取與使用。
讓我們來看看下面的程式碼:
function CoolModule() {
var something = "cool";
var another = [1, 2, 3];
function doSomething() {
console.log( something );
}
function doAnother() {
console.log( another.join( " ! " ) );
}
return {
doSomething: doSomething,
doAnother: doAnother
};
}
var foo = CoolModule();
foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3
以上程式碼中,CoolModule
最後返回了一個物件,它具有以下幾個特性:
doSomething
、doAnother
的引用像上面這樣一個物件,即是一個模組的公用 API。
API(Application Programming Interface)這個概念是和模組一同出現的,它在中文被翻譯成「應用程式介面」或「應用程序接口」,通常是指模組(或某個系統)暴露給外部取用的部分。
在使用造模組的過程中,有以下幾點要注意:
模組另一個強大之處,就在於可以從外部改變內部函式的變數值,而這也是 API 具備的功能之一。
API 向外界開放了函式/物件內部的參考,因此能夠改變這個模組,包括添加或刪除方法(method)、屬性(Property),或改變它們的值。
var foo = (function CoolModule(id) {
function change() {
// 修改公有 API
publicAPI.identify = identify2;
}
function identify1() {
console.log( id );
}
function identify2() {
console.log( id.toUpperCase() );
}
var publicAPI = {
change: change,
identify: identify1
};
return publicAPI;
})( "foo module" );
foo.identify(); // foo module
foo.change();
foo.identify(); // FOO MODULE
如果只需要一個模組實例,可以使用 IIFE:
var foo = (function CoolModule() {
var something = "cool";
var another = [1, 2, 3];
function doSomething() {
console.log( something );
}
function doAnother() {
console.log( another.join( " ! " ) );
}
return {
doSomething: doSomething,
doAnother: doAnother
};
})();
foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3
模組模式(Module Pattern)是當前常見的一種程式設計模式,利用閉包的特性,將方法和變數限制在一個範圍內存取和使用。
const counter = (function () {
// private variable
let i = 0;
// private function
const sayTheNumber = function () {
console.log(`Now it's ${i}`)
};
// public function
return {
get: function () {
return i;
},
set: function (val) {
i = val;
sayTheNumber();
},
increment: function () {
return ++i;
},
decrement: function () {
return --i;
}
};
})();
console.log(counter.get()); // 0
console.log(counter.decrement()); // -1
counter.set(3); // Now it's 3
console.log(counter.increment()); // 4
console.log(counter.increment()); // 5
ES6 以後,JS 便開始將一個文件視為一個獨立的模組,每個模組(文件)能夠導入其它模組或特定的 API,也可以導出它自己公有的 API。
由於模組的 API 也能在執行時期被改變,所以基於函式的模組模式無法進行靜態分析(編輯器無法知道模組的內容)。
但 ES6 的模組 API 是靜態的,不會在運行時改變,它在編譯時期就會檢查被導入的模組 API 成員是否存在,如果該 API 引用不存在,編譯器就會提早拋出一個錯誤,而不是等到執行時期才發現。
ES6 的每個模組必須定義在一個單獨的文件中,瀏覽器或 JS 引擎擁有自己的「模組加載器」,在模組被導入時會同步加載該模組文件。
在一個「模組文件」內部的內容就像包裹在函式內的作用域,這些被導出的 API 同樣具有閉包特性,就像使用函式的閉包一樣。
以下是範例:
【bar.js】
function hello(who) {
return "Let me introduce: " + who;
}
export hello;
【foo.js】
// 導入 bar 模組中的 "hello()"
import hello from "bar";
var hungry = "hippo";
function awesome() {
console.log(
hello( hungry ).toUpperCase()
);
}
export awesome;
【index.js】
// 導入 bar 和 foo 整個模組
import foo from "foo";
import bar from "bar";
console.log(
bar.hello( "rhino" )
);
// Let me introduce: rhino
foo.awesome();
// LET ME INTRODUCE: HIPPO